'''
console.py

Copyright 2009 Xavier Mendez Navarro aka Javi

This file is part of pysqlin

pysqlin is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation version 2 of the License.

pysqlin is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with pysqlin; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
'''

READLINE = True
try:
    import readline
except:
    READLINE = False
    pass
    
import logging
import re
import string
import sys
import shlex
from types import *

import cStringIO,operator
import math

from ui.commander import CommandParser
from framework.roles import Notifier
from framework.roles import Registrant
from framework.settings import Settings

class Console(Notifier, Registrant):
    EXIT_EVENT = 'COMMAND root.exit'
    SECTION_EVENT = 'COMMAND root.section'
    KBASE_EVENT = 'KBASE root'

    def __init__(self, prompt, filename):
	self.__logger = logging.getLogger("ui.console.Console")

	self.prompt = prompt
	self.subprompt = ""

	self.commands = CommandParser(filename)
	self.create_event(self.EXIT_EVENT) # console exit event
	self.create_event(self.KBASE_EVENT) # kbase event
	self.create_event(self.SECTION_EVENT) # changing section event

        self.current_candidates = []
	self.current_node = self.commands.commands

    # ------------------------------------------------
    # command management
    # ------------------------------------------------
    def add_command(self, full_section, command_name, help, usage, parameters, label):
	self.commands.add_command(full_section, command_name, help, usage, parameters, label)

    def delete_commands(self, full_section, label):
	self.commands.delete_commands(full_section, label)

    # ------------------------------------------------
    # event handling
    # ------------------------------------------------
    def send_param_event(self, pos, param_value, command_params, line):
	param_name = command_params[pos]['name']
	self.__logger.debug('send_param_event. START, pos=%d, name=%s, value=%s' % (pos, param_name, param_value))

	cmd, parameters = self.split_command(' '.join(line))

	n = 0
	param_data = {}
	for p in parameters:
	    param_data[command_params[n+1]['name']] = parameters[n]
	    n = n + 1

	if pos == 1:
	    event_name = (self.current_node['fullname'], cmd, param_name)
	else:
	    event_name = (self.current_node['fullname'], cmd, param_name, param_value)

	self.__logger.debug('send_param_event. Event=%s', '.'.join(event_name))

	return self.notify('PARAMETER ' + '.'.join(event_name),
		    section = self.current_node['name'],
		    cmd = cmd,
		    current = param_name,
		    current_val = param_value,
		    parameters = param_data)

    def send_cmd_event(self, current_command, cmd, parameters):
	# construct event data dictionary
	n = 0
	param_data = {}
	for p in parameters:
	    param_data[current_command['parameters'][n+1]['name']] = parameters[n]
	    n = n + 1

	event_name = self.current_node['fullname'] + '.' + cmd 
	self.__logger.debug('send_cmd_event. NOTIFY: COMMAND %s data=%s' % (event_name, param_data))
	msg = self.notify('COMMAND ' + event_name, 
	    section = self.current_node['name'],
	    cmd = cmd,
	    parameters = param_data)
	if msg:
	    print "/* Command error: %s." % (msg)


    def send_kb_event(self, pos, words, name):
	self.__logger.debug('send_kb_event. START')

	# send 1st parameter name and value for the rest
	if pos == 1:
	    event = name
	else:
	    event = '.'.join(words[1:-1])
	    event = event + '.' + name
						
	self.__logger.debug('send_kb_event. Event=%s', event)

	return self.notify(self.KBASE_EVENT,
	    key=event)

    # ------------------------------------------------
    # Readline functions
    # ------------------------------------------------
    def get_candidates(self):
	#try:
	candidates = []
	# add below sections
	if self.current_node.has_key('sections'):
	    candidates = sorted(self.current_node['sections'].keys())

	# add allowed commands
	if self.current_node.has_key('commands'):
	    candidates += sorted(self.current_node['commands'].keys())

	#candidates.append('_help_')
	# If section not root, we can exit
	if self.current_node['name'] != 'root':
	    candidates.append('exit')
	#except KeyError, err:
	#    self.__logger.error('get_candidates. EXCEPTION: Key error: %s', err)
	#    candidates = []

	return candidates

    def completer(self, text, state):
        response = None
        if state == 0:
            # This is the first time for this text, so build a match list.
            
            origline = readline.get_line_buffer()
            begin = readline.get_begidx()
            end = readline.get_endidx()
            being_completed = origline[begin:end]
            words = origline.split(' ')
	    lwords = len(words)

            #self.__logger.debug('completer. Current_node=%s', self.current_node)
            self.__logger.debug('completer. State=%d', state)
            self.__logger.debug('completer. Origline=%s', repr(origline))
            self.__logger.debug('completer. Begin=%s', begin)
            self.__logger.debug('completer. End=%s', end)
            self.__logger.debug('completer. Being_completed=%s', being_completed)
            self.__logger.debug('completer. Words=%s', words)
            self.__logger.debug('completer. Len_words=%s', len(words))
            self.__logger.debug('completer. First=%s', words[0])

            if not words:
                self.current_candidates = self.get_candidates()
            else:
                #try:
		if lwords == 1:
		    candidates = self.get_candidates()
		else:
		    first = words[0]
		    candidates = []

		    if first in self.current_node['commands'].keys():
			current_command = self.current_node['commands'][first]
			current_parameters = current_command['parameters']
			self.__logger.debug('completer. current_parameters=%s', current_parameters)
			values = current_parameters[lwords-1]['values']
			self.__logger.debug('completer. values=%s', values)
			if values == '{@event@}':
			    self.__logger.info('completer. event value')
			    candidates = self.send_param_event(
				    lwords - 1,
				    words[lwords-2],
				    current_parameters,
				    words)
			elif type(values) is not ListType and values.startswith('{@KB:'):
			    self.__logger.info('completer. KB value')
			    nothing, name = values.split(':')

			    candidates = self.send_kb_event(lwords-1, words, name[:-2])
			elif type(values) is StringType or type(values) is UnicodeType:
			    raise Exception, "Error reading parameter type"
			else:
			    self.__logger.info('completer. standart value')
			    candidates = current_parameters[lwords-1]['values']

			#if lwords == 2: candidates.append('_help_')

		if being_completed:
		    # match options with portion of input
		    # being completed
		    self.current_candidates = [ w for w in candidates
						if w.startswith(being_completed) ]
		else:
		    # matching empty string so use all candidates
		    self.current_candidates = candidates

		self.__logger.debug('completer. Candidates=%s', self.current_candidates)
        #        except Exception, err:
        #            self.__logger.error('completer. EXCEPTION: Completion error: %s', err)
        #            self.current_candidates = []
	#	    #raise Exception, 'get_candidates. err: %s' % (err)

        try:
            response = self.current_candidates[state]
        except IndexError:
	    self.__logger.error('Completer. EXCEPTION: Index error: %s', err)
            response = None
        self.__logger.debug('Completer. complete(%s, %s) => %s', repr(text), state, response)
        return response

    def update_prompt(self):
	if self.current_node['name'] == 'root':
	    self.subprompt = ''
	    return

	full = self.current_node['fullname'].split('.')[1:]
	self.subprompt = '(' + '.'.join(full) + ')'

    def split_command(self, line):
	try:
	    words = shlex.split(line)
	except Exception, e:
	    print "Argument error: %s" % e
	    return (None, None)
	    
	if not words:
	    return (None, None)

	cmd, parameters = words[0].lower(),words[1:]

	return (cmd, parameters)

    def execute(self, cmd):
	cmd, parameters = self.split_command(cmd)
	if cmd is None:
	    return

	self.__logger.debug('execute. execute? cmd=%s, parameters=%s' % (cmd, parameters))

	# exit section
	if cmd == 'exit' and self.current_node['name'] != 'root':
	    self.current_node = self.commands.goto_previous(self.current_node['name'])
	    self.update_prompt()
	    self.notify(self.SECTION_EVENT, section=self.current_node['fullname'])
	    return

	# enter section
	if self.current_node.has_key('sections'):
	    if unicode(cmd, 'utf-8') in self.current_node['sections'].keys():
		self.current_node = self.current_node['sections'][cmd]
		self.update_prompt()
		self.notify(self.SECTION_EVENT, section=self.current_node['fullname'])

		return

	# current_command
	current_command = None 
	if self.current_node.has_key('commands'):
	    if unicode(cmd, 'utf-8') in self.current_node['commands'].keys():
	    	current_command = self.current_node['commands'][cmd]
	    elif cmd != '?':
		print "/* No such command, type ? for help."
		return

	# help
	if current_command:
	    if parameters and parameters[-1] == '?' and current_command.has_key('usage'):
		print "/* Usage: " + current_command['usage']
		return
	else:
	    if cmd == '?':
		if self.current_node.has_key('sections'):
		    print '\nSections:\n'
		    for i in self.current_node['sections'].values():
			if i.has_key('help'):
			    print '/* %-50s ==> %-50s' % (i['name'],i['help'])

		if self.current_node.has_key('commands'):
		    print '\nCore commands:\n'
		    for i in self.current_node['commands'].values():
			if i.has_key('help') and not i.has_key('label'):
			    print '/* %-50s ==> %-50s' % (i['usage'],i['help'])
		    print '\nPlugin commands:\n'
		    for i in self.current_node['commands'].values():
			if i.has_key('help') and i.has_key('label'):
			    print '/* %-50s ==> %-50s' % (i['usage'],i['help'])
		    print '\n'
		return

	# Has default value for last param?
	current_comm_len = len(self.current_node['commands'][cmd]['parameters'])
	default = None
	if current_comm_len > 0 and current_command['parameters'][current_comm_len].has_key('default'):
	    default = current_command['parameters'][current_comm_len]['default']

	# execute command
	if current_command:
	    # test for completeness
	    if default is not None and current_comm_len - 1 == len(parameters):
		parameters.append(default)
	    elif current_comm_len != len(parameters):
		print "/* Command error, type ? for help. Usage: %s" % (current_command['usage'])
		return

	self.send_cmd_event(current_command, cmd, parameters)


    # ------------------------------------------------
    # view functions
    # ------------------------------------------------
    def output(self, msg):
    	print msg

    def ask_for_exit(self):
	try:
	    print
	    answer = raw_input('Are you sure you want to exit (y/n)?')
	except EOFError:
	    return 'y'
	except KeyboardInterrupt:
	    print
	    return 'n'

	return answer

    def indent(self, rows, hasHeader=False, headerChar='-', delim=' | ', justify='left',
	    separateRows=False, prefix='', postfix='', wrapfunc=lambda x:x):
	"""
	@author http://code.activestate.com/recipes/267662-table-indentation/

	
	Indents a table by column.
	- rows: A sequence of sequences of items, one sequence per row.
	- hasHeader: True if the first row consists of the columns' names.
	- headerChar: Character to be used for the row separator line
	    (if hasHeader==True or separateRows==True).
	- delim: The column delimiter.
	- justify: Determines how are data justified in their column. 
	    Valid values are 'left','right' and 'center'.
	- separateRows: True if rows are to be separated by a line
	    of 'headerChar's.
	- prefix: A string prepended to each printed row.
	- postfix: A string appended to each printed row.
	- wrapfunc: A function f(text) for wrapping text; each element in
	    the table is first wrapped by this function."""
	# closure for breaking logical rows to physical, using wrapfunc
	def rowWrapper(row):
	    newRows = [wrapfunc(item).split('\n') for item in row]
	    return [[substr or '' for substr in item] for item in map(None,*newRows)]
	# break each logical row into one or more physical ones
	logicalRows = [rowWrapper(row) for row in rows]
	# columns of physical rows
	columns = map(None,*reduce(operator.add,logicalRows))
	# get the maximum of each column by the string length of its items
	maxWidths = [max([len(str(item)) for item in column]) for column in columns]
	rowSeparator = headerChar * (len(prefix) + len(postfix) + sum(maxWidths) + \
				    len(delim)*(len(maxWidths)-1))
	# select the appropriate justify method
	justify = {'center':str.center, 'right':str.rjust, 'left':str.ljust}[justify.lower()]
	output=cStringIO.StringIO()
	if separateRows: print >> output, rowSeparator
	for physicalRows in logicalRows:
	    for row in physicalRows:
		print >> output, \
		    prefix \
		    + delim.join([justify(str(item),width) for (item,width) in zip(row,maxWidths)]) \
		    + postfix
	    if separateRows or hasHeader: print >> output, rowSeparator; hasHeader=False
	return output.getvalue()

    def wrap_always(self, text, width):
	"""A simple word-wrap function that wraps text on exactly width characters.
	It doesn't split the text in words."""
	return '\n'.join([ text[width*i:width*(i+1)] \
			for i in xrange(int(math.ceil(1.*len(text)/width))) ])

    def table_print(self, rows):
	width = int(Settings().get(Settings.SEC_GRL, Settings.TABLE_WIDTH))
	print self.indent(rows, hasHeader=True, separateRows=False,
                     prefix='  ', postfix='  ',
                     wrapfunc=lambda x: self.wrap_always(x,width))

#    def table_print(self, rows, header=True, vdelim=" | ", padding=1, justify='left'):
#        """ Outputs a list of lists as a Restructured Text Table
#
#        @param rows list of lists
#        @param header if True the first row is treated as a table header
#        @param vdelim vertical delimiter betwee columns
#        @param padding padding nr. of spaces are left around the longest element in the column
#        @param justify may be left,center,right
#
#	@author http://code.activestate.com/recipes/267662/
#        """
#        border="-" # character for drawing the border
#        justify = {'left':string.ljust,'center':string.center, 'right':string.rjust}[justify.lower()]
#
#        # calculate column widhts (longest item in each col
#        # plus "padding" nr of spaces on both sides)
#        cols = zip(*rows)
#        colWidths = [max([len(str(item))+2*padding for item in col]) for col in cols]
#
#        # the horizontal border needed by rst
#        #borderline = vdelim.join([w*border for w in colWidths])
#        borderline = '-+-'.join([w*border for w in colWidths])
#
#        # outputs table in rst format
#        print '+ ' + borderline + ' +'
#        for row in rows:
#            print '| ' + vdelim.join([justify(str(item),width) for (item,width) in zip(row,colWidths)]) + " |"
#            if header: print '+ ' + borderline + ' +'; header=False
#        print '+ ' + borderline + ' +'

    def clear_line(self):
	sys.stdout.write("\x1B[2K") # erase line
	sys.stdout.write("\x1B[0G") # move cursor to the beginning
	sys.stdout.flush()

    def print_char(self, position, charac):
	filter = ''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])

	sys.stdout.write("\x1b[%dG%s" % (position,charac.translate(filter)))
	sys.stdout.flush()

    # ------------------------------------------------
    # Main loop
    # ------------------------------------------------
    def run(self):
	if READLINE:
	    readline.set_completer_delims("`~!@#$%^&*()-=+[ {]}\|;:'\",<>?")
	    readline.parse_and_bind('tab: complete')
	    readline.set_completer(self.completer)
	else:
	    print "\n\nThis is gonna be harder without readline...\n"

	while True:
	    try:
		command=raw_input(self.prompt + self.subprompt + ">")
		self.execute(command)
	    except KeyboardInterrupt:
		if self.ask_for_exit() == 'y': break
	    except EOFError:
		if self.current_node['name'] != 'root':
		    print
		    self.execute('exit')
		else:
		    if self.ask_for_exit() == 'y': break

	self.notify(self.EXIT_EVENT)
	print "\r\n\r\nBye!..."
